<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Audio Bars – Spectrum Visualizer</title>
<style>
  html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000;}
  #box{width:100%;height:100%;}
</style>

<!-- 本地 three 裸模块映射 -->
<script type="importmap">
{
  "imports": {
    "three": "./threejs/build/three.module.js",
    "three/addons/": "./threejs/examples/jsm/"
  }
}
</script>
</head>
<body>
<div id="box"></div>

<script type="module">
import {
  Vector3,
  Mesh,
  BoxGeometry,
  Color,
  WebGLRenderer,
  PCFSoftShadowMap,
  OrthographicCamera,
  ShaderMaterial,
  Scene
} from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

/* ---------- 工具函数 ---------- */
function initOrthographicCamera(initialPosition = new Vector3(0, 0, 100)) {
  const s = 15;
  const h = window.innerHeight;
  const w = window.innerWidth;
  const camera = new OrthographicCamera(
    -s, s, s * (h / w), -s * (h / w), 1, 10000
  );
  camera.position.copy(initialPosition);
  camera.up.set(0, 0, 1);
  camera.zoom = 0.4;
  camera.lookAt(0, 0, 0);
  camera.updateProjectionMatrix();
  return camera;
}

function initRenderer(props = {}) {
  const dom = document.getElementById('box');
  const renderer = new WebGLRenderer({ antialias: true, ...props });
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = PCFSoftShadowMap;
  renderer.setPixelRatio(devicePixelRatio);
  renderer.setSize(dom.offsetWidth, dom.offsetHeight);
  dom.appendChild(renderer.domElement);
  return renderer;
}

/* ---------- 着色器 ---------- */
const vertexShader = /* glsl */`
  varying vec4 vPosition;
  void main() {
    vPosition = modelMatrix * vec4(position, 1.0);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragmentShader = /* glsl */`
  varying vec4 vPosition;
  uniform float uScale;
  uniform vec3 colors[6];
  void main() {
    float intensity = clamp((vPosition.y + 13.0) * uScale / 255.0, 0.0, 1.0);
    float segment   = intensity * 5.0;
    int   index     = int(segment);
    float t         = fract(segment);
    vec3  col       = mix(colors[index], colors[index + 1], t);
    gl_FragColor    = vec4(col, 1.0);
  }
`;

/* ---------- 主逻辑 ---------- */ 
const audioURL  = FILE_HOST + 'files/audio/YMCA.mp3';

const colors = [
  new Color(0.5, 0.0, 1.0), // 紫
  new Color(0.0, 0.0, 1.0), // 蓝
  new Color(0.0, 1.0, 1.0), // 青
  new Color(0.0, 1.0, 0.0), // 绿
  new Color(1.0, 1.0, 0.0), // 黄
  new Color(1.0, 0.0, 0.0)  // 红
];

const renderer = initRenderer();
const camera   = initOrthographicCamera();
const scene    = new Scene();

const orbitControls = new OrbitControls(camera, renderer.domElement);

/* 柱体 */
const barCount  = 64;
const geometry  = new BoxGeometry(0.5, 1, 0.5);
const material  = new ShaderMaterial({
  uniforms:{
    colors:{ value:colors },
    uScale:{ value:1.0 }
  },
  vertexShader,
  fragmentShader
});

const bars = [];
for (let i = 0; i < barCount; i++) {
  const offset = i - barCount / 2;
  const bar = new Mesh(geometry, material.clone());
  bar.position.set(offset, -12.5, 0);
  scene.add(bar);
  bars.push(bar);

  const mirror = new Mesh(geometry, material.clone());
  mirror.position.set(-offset, -12.5, 0);
  scene.add(mirror);
  bars.push(mirror);
}

/* 音频 */
const audio = new Audio(audioURL);
audio.crossOrigin = 'anonymous';

const audioCtx  = new (window.AudioContext || window.webkitAudioContext)();
const analyser  = audioCtx.createAnalyser();
const source    = audioCtx.createMediaElementSource(audio);
source.connect(analyser);
source.connect(audioCtx.destination);

analyser.fftSize = barCount * 2;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

/* 更新函数 */
function updateBars() {
  analyser.getByteFrequencyData(dataArray);
  const len = bars.length - 1;
  for (let i = 0, j = len; i < barCount; i++, j = len - i) {
    const scale   = dataArray[barCount - 1 - i] / 10;
    const height  = Math.max(1, scale);
    bars[i].scale.y = height;
    bars[j].scale.y = height;
    bars[i].position.y = bars[j].position.y = -13 + height / 2;
    bars[i].material.uniforms.uScale.value = scale;
    bars[j].material.uniforms.uScale.value = scale;
  }
}

/* 渲染循环 */
function render() {
  requestAnimationFrame(render);
  updateBars();
  orbitControls.update();
  renderer.render(scene, camera);
}
render();

/* GUI */
const gui = new GUI({ title:'Audio Control' });
gui.add({ play:()=>audioCtx.resume().then(()=>audio.play()) }, 'play').name('▶ Play');
gui.add({ pause:()=>audio.pause() }, 'pause').name('⏸ Pause');

/* 自适应 */
addEventListener('resize', () => {
  camera.left   = -15;
  camera.right  =  15;
  camera.top    =  15 * (innerHeight / innerWidth);
  camera.bottom = -15 * (innerHeight / innerWidth);
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

/* 控制台水印 */
console.log('%cthreelab Visualizer © 2024', 'color:#8ff;font-size:14px;background:#000;padding:4px 8px;border-radius:4px;');
</script>
</body>
</html>